﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace SFP.Animation2D
{
    public class ParticleSystem : LayerObject
    {
        public const int PARTICLE_CIRCLE = 0;
        public const int PARTICLE_SQUARE = 1;
        public const int PARTICLE_CROSS = 2;
        public const int PARTICLE_IMAGE = 3;

        private Emitter emitter;
        private HashSet<Particle> particles;
        private HashSet<Particle> deadParticles;
        private Matrix matrix;
        private ColorMatrix colorMatrix;
        private ImageAttributes imageAttributes;

        private long particalCount;

        private int particleType = PARTICLE_CIRCLE;
        private Image particleImg = null;
        private string particleImgPath = "";
        private double liveTime = 0;
        private double liveTimeRan = 0;
        private double size = 0;
        private double sizeRan = 0;
        private double rotation = 0;
        private double rotationRan = 0;
        private double rotationSpeed = 0;
        private double rotationSpeedRan = 0;
        private double opacity = 100;
        private double opacityRan = 0;
        private Color color;
        public bool rotateFromMotion = false;

        private double gravity = 0;

        public ParticleSystem()
        {
            this.emitter = new Emitter(0, 0, Emitter.POINT, this);

            particles = new HashSet<Particle>();
            deadParticles = new HashSet<Particle>();
            matrix = new Matrix();
            colorMatrix = new ColorMatrix();
            imageAttributes = new ImageAttributes();

            color = Color.White;
        }

        // Getter...
        public Emitter GetEmitter() { return emitter; }

        public long GetParticleCount() { return particalCount; }

        public int GetParticleType() { return particleType; }

        public string GetParticleImagePath() { return particleImgPath; }

        public Image GetParticleImage() { return particleImg; }

        public double GetLiveTime() { return liveTime; }

        public double GetLiveTimeRan() { return liveTimeRan; }

        public double GetSize() { return size; }

        public double GetSizeRan() { return sizeRan; }

        public double GetRotation() { return rotation; }

        public double GetRotationRan() { return rotationRan; }

        public double GetRotationSpeed() { return rotationSpeed; }

        public double GetRotationSpeedRan() { return rotationSpeedRan; }

        public double GetOpacity() { return opacity; }

        public double GetOpacityRan() { return opacityRan; }

        public Color GetColor() { return color; }

        public double GetGravity() { return gravity; }

        // Setter...
        public void SetParticleType(int particleType)
        {
            this.particleType = particleType;
        }

        public void SetParticleImage(string particleImgPath)
        {
            particleImg = ContentLoader.LoadBitmap(particleImgPath);
            if (particleImg != null) this.particleImgPath = particleImgPath;
        }

        public void SetLiveTime(double liveTime)
        {
            this.liveTime = liveTime;
        }

        public void SetLiveTimeRan(double liveTimeRan)
        {
            this.liveTimeRan = liveTimeRan;
        }

        public void SetSize(double size)
        {
            this.size = size;
        }

        public void SetSizeRan(double sizeRan)
        {
            this.sizeRan = sizeRan;
        }

        public void SetRotation(double rotation)
        {
            this.rotation = rotation - (360 * (long)(rotation / 360));
            if (rotation < 0) rotation *= -1;
        }

        public void SetRotationRan(double rotationRan)
        {
            this.rotationRan = rotationRan;
        }

        public void SetRotationSpeed(double rotationSpeed)
        {
            this.rotationSpeed = rotationSpeed;
        }

        public void SetRotationSpeedRan(double rotationSpeedRan)
        {
            this.rotationSpeedRan = rotationSpeedRan;
        }

        public void SetOpacity(double opacity)
        {
            this.opacity = opacity;
        }

        public void SetOpacityRan(double opacityRan)
        {
            this.opacityRan = opacityRan;
        }

        public void SetColor(Color color)
        {
            this.color = color;

            foreach (Particle particle in particles)
            {
                Color pColor = Color.FromArgb((int)(255 * (particle.opacity / 100)), color.R, color.G, color.B);
                particle.brush.Color = pColor;
                particle.pen.Color = pColor;
            }
        }

        public void SetGravity(double gravity)
        {
            this.gravity = gravity;
        }

        public void loadPreset(PSPreset preset)
        {
            emitter.linearEmittInterpolation = preset.linearEmittInterpolation;
            emitter.SetAmountPerSec(preset.amountPerSec);
            emitter.SetVelocity(preset.velocity);
            emitter.SetVelocityRan(preset.velocityRan);
            emitter.SetDirection(preset.direction);
            emitter.SetDirectionRan(preset.directionRan);
            SetParticleType(preset.particleType);
            SetParticleImage(preset.particleImgPath);
            SetLiveTime(preset.liveTime);
            SetLiveTimeRan(preset.liveTimeRan);
            SetSize(preset.size);
            SetSizeRan(preset.sizeRan);
            SetRotation(preset.rotation);
            SetRotationRan(preset.rotationRan);
            SetRotationSpeed(preset.rotationSpeed);
            SetRotationSpeedRan(preset.rotationSpeedRan);
            SetOpacity(preset.opacity);
            SetOpacityRan(preset.opacityRan);
            SetColor(preset.color);
            SetGravity(preset.gravity);
        }

        public PSPreset createPreset()
        {
            PSPreset preset = new PSPreset();
            preset.linearEmittInterpolation = emitter.linearEmittInterpolation;
            preset.amountPerSec = emitter.GetAmountPerSec();
            preset.velocity = emitter.GetVelocity();
            preset.velocityRan = emitter.GetVelocityRan();
            preset.direction = emitter.GetDirection();
            preset.directionRan = emitter.GetDirectionRan();
            preset.particleType = particleType;
            preset.particleImgPath = particleImgPath;
            preset.liveTime = liveTime;
            preset.liveTimeRan = liveTimeRan;
            preset.size = size;
            preset.sizeRan = sizeRan;
            preset.rotation = rotation;
            preset.rotationRan = rotationRan;
            preset.rotationSpeed = rotationSpeed;
            preset.rotationSpeedRan = rotationSpeedRan;
            preset.opacity = opacity;
            preset.opacityRan = opacityRan;
            preset.color = color;
            preset.gravity = gravity;

            return preset;
        }

        public void update(double time)
        {
            foreach (Particle particle in particles)
            {
                particle.update(time);
            }

            emitter.emitt(time);

            foreach (Particle particle in deadParticles)
            {
                particles.Remove(particle);
            }
            deadParticles.Clear();

            particalCount = particles.Count;
        }

        public override void draw(Graphics g)
        {
            foreach (Particle particle in particles)
            {
                particle.draw(g);
            }
        }

        public class Emitter
        {
            public const int POINT = 0;
            public const int SQUARE = 1;
            public const int CIRCLE = 2;

            private ParticleSystem pSystem;

            private double emittTime = 0;

            public double x;
            public double y;
            private double x_last_emitt;
            private double y_last_emitt;

            public int type = POINT;
            public bool linearEmittInterpolation = false;
            private double amountPerSec = 0;
            private double velocity = 0;
            private double velocityRan = 0;
            private double direction = 0;
            private double directionRan = 100;

            private Random velocityRanObj;
            private Random directionRanObj;
            private Random liveTimeRanObj;
            private Random sizeRanObj;
            private Random rotationRanObj;
            private Random rotationSpeedRanObj;
            private Random opacityRanObj;

            internal Emitter(double x, double y, int type, ParticleSystem pSystem)
            {
                this.x = x;
                this.y = y;
                this.x_last_emitt = x;
                this.y_last_emitt = y;
                this.type = type;
                this.pSystem = pSystem;

                Random randomSeed = new Random();
                velocityRanObj = new Random(randomSeed.Next(int.MaxValue));
                directionRanObj = new Random(randomSeed.Next(int.MaxValue));
                liveTimeRanObj = new Random(randomSeed.Next(int.MaxValue));
                sizeRanObj = new Random(randomSeed.Next(int.MaxValue));
                rotationRanObj = new Random(randomSeed.Next(int.MaxValue));
                rotationSpeedRanObj = new Random(randomSeed.Next(int.MaxValue));
                opacityRanObj = new Random(randomSeed.Next(int.MaxValue));
            }

            // Getter
            public double GetAmountPerSec() { return amountPerSec; }
            
            public double GetVelocity() { return velocity; }

            public double GetVelocityRan() { return velocityRan; }
            
            public double GetDirection() { return direction; }

            public double GetDirectionRan() { return directionRan; }

            // Setter
            public void SetAmountPerSec(double amountPerSec)
            {
                this.amountPerSec = amountPerSec;
            }

            public void SetVelocity(double velocity)
            {
                this.velocity = velocity;
            }

            public void SetVelocityRan(double velocityRan)
            {
                this.velocityRan = velocityRan;
            }

            public void SetDirection(double direction)
            {
                this.direction = direction - (360 * (long)(direction / 360));
                if (direction < 0) direction *= -1;
            }

            public void SetDirectionRan(double directionRan)
            {
                this.directionRan = directionRan;
            }

            public void emitt(int amount)
            {
                for (int i = 0; i < amount; i++)
                {
                    emittParticle(i, amount, 0, false);
                }
            }

            internal void emitt(double time)
            {
                if (amountPerSec == 0) return;
                
                emittTime += time;
                
                double nextParticleTime = 1000 / amountPerSec;
                int amount = (int)(emittTime / nextParticleTime);

                emittTime -= amount * nextParticleTime;
                
                for (int i = 0; i < amount; i++)
                {
                    emittParticle(i, amount, nextParticleTime, linearEmittInterpolation);
                }

                x_last_emitt = x;
                y_last_emitt = y;
            }

            private void emittParticle(int index, int emittAmount, double nextParticleTime, bool linearEmittInterpolation)
            {
                if (pSystem.particles.Count == Int32.MaxValue) return;

                double pVelocity = randomValue(velocity, velocityRan, velocityRanObj);
                double pDirection = randomDegree(direction, directionRan, directionRanObj);
                double pLiveTime = randomValue(pSystem.liveTime, pSystem.liveTimeRan, liveTimeRanObj);
                double pSize = randomValue(pSystem.size, pSystem.sizeRan, sizeRanObj);
                double pRotation = randomDegree(pSystem.rotation, pSystem.rotationRan, rotationRanObj);
                double pRotationSpeed = randomValue(pSystem.rotationSpeed, pSystem.rotationSpeedRan, rotationSpeedRanObj);
                double pOpacity = randomPercent(pSystem.opacity, pSystem.opacityRan, opacityRanObj);
                Color pColor = Color.FromArgb((int)(255 * (pOpacity / 100)), pSystem.color.R, pSystem.color.G, pSystem.color.B);
                double pX;
                double pY;

                if (linearEmittInterpolation)
                {
                    pX = x - (x - x_last_emitt) * (index / (double)emittAmount);
                    pY = y - (y - y_last_emitt) * (index / (double)emittAmount);
                }
                else
                {
                    pX = x;
                    pY = y;
                }

                Particle particle = new Particle(pSystem, pX, pY, pDirection, pVelocity, pLiveTime,
                                                                               pSize, pRotation, pRotationSpeed, pOpacity, pColor);
                particle.update(index * nextParticleTime + emittTime);
                pSystem.particles.Add(particle);
            }

            private double randomValue(double refValue, double randomness, Random ranObj)
            {
                return refValue + (refValue * (randomness / 100) * 2 * ranObj.NextDouble() - refValue * (randomness / 100));
            }

            private double randomDegree(double refDegree, double randomness, Random ranObj)
            {
                return refDegree + (ranObj.NextDouble() * ((randomness / 100) * 360) - ((randomness / 100) * 360) / 2);
            }

            private double randomPercent(double refValue, double randomness, Random ranObj)
            {
                if (refValue < randomness / 2) refValue = randomness / 2;
                else if (refValue > 100 - (randomness / 2)) refValue = 100 - (randomness / 2);

                double codomainDistToZero = refValue - (refValue - randomness / 2);

                return refValue + randomValue(randomness / 2, randomness, ranObj) - codomainDistToZero;
            }
        }

        private class Particle
        {
            ParticleSystem pSystem;
            
            double x;
            double y;
            double lastDirection;
            double velocityX;
            double velocityY;
            double liveTime;
            double size;
            double rotation;
            double rotationSpeed;
            internal double opacity;

            internal SolidBrush brush;
            internal Pen pen;

            double timePassed;

            internal Particle(ParticleSystem pSystem, double x, double y, double direction, double velocity, double liveTime, double size, double rotation, double rotationSpeed, double opacity, Color color)
            {
                this.pSystem = pSystem;
                this.x = x;
                this.y = y;
                this.lastDirection = direction;
                this.liveTime = liveTime;
                this.size = size;
                if (pSystem.rotateFromMotion)
                    this.rotation = direction + rotation;
                else
                    this.rotation = rotation;
                this.rotationSpeed = rotationSpeed;
                this.opacity = opacity;
                this.brush = new SolidBrush(color);
                this.pen = new Pen(brush, (float)size / 10);
                this.timePassed = 0;

                // Sin(...) and Cos(...) uses radians, not degrees!!! (π/180 equals 1 degree)
                this.velocityX = velocity * Math.Cos(direction * Math.PI / 180);
                this.velocityY = velocity * Math.Sin(direction * Math.PI / 180);
            }

            public void update(double time)
            {
                // Life
                timePassed += time / 1000;

                if (timePassed >= liveTime)
                {
                    pSystem.deadParticles.Add(this);
                    return;
                }

                // Gravity
                velocityY -= pSystem.gravity * time / 1000;

                // Position
                x += velocityX * time / 1000;
                y -= velocityY * time / 1000;
                
                // Rotation from motion
                double currentDirection = GetCurrentDirection();
                if (pSystem.rotateFromMotion)
                {
                    rotation += currentDirection - lastDirection;
                }

                // Rotation Speed
                rotation += rotationSpeed * time * 0.0045;

                // Remember Direction
                lastDirection = currentDirection;
            }

            private double GetCurrentDirection()
            {
                double currentVelocity = Math.Sqrt(square(velocityX) + square(velocityY));
                double currentDirection = Math.Acos(velocityX / currentVelocity) / (Math.PI / 180);

                if (velocityY < 0) return currentDirection * -1 + 180;
                return currentDirection;
            }

            private double square(double value)
            {
                return value * value;
            }

            public void draw(Graphics g)
            {
                double wX = pSystem.X + x;
                double wY = pSystem.Y + y;
                float x_left = (float)(wX - size / 2);
                float y_top = (float)(wY - size / 2);

                Matrix mx = pSystem.matrix;
                mx.RotateAt((float)rotation * -1, new PointF((float)wX, (float)wY));
                g.Transform = mx;

                int particleType = pSystem.particleType;
                Image image = pSystem.particleImg;

                if (particleType == ParticleSystem.PARTICLE_CIRCLE)
                {
                    g.FillEllipse(brush, x_left, y_top, (float)size, (float)size);
                }
                else if (particleType == ParticleSystem.PARTICLE_SQUARE)
                {
                    g.FillRectangle(brush, x_left, y_top, (float)size, (float)size);
                }
                else if (particleType == ParticleSystem.PARTICLE_CROSS)
                {
                    g.DrawLine(pen, new PointF((float)(wX - size / 2), (float)wY), new PointF((float)(wX + size / 2), (float)wY));
                    g.DrawLine(pen, new PointF((float)wX, (float)(wY - size / 2)), new PointF((float)wX, (float)(wY + size / 2)));
                }
                else if (particleType == ParticleSystem.PARTICLE_IMAGE)
                {
                    if (image != null)
                    {
                        ImageAttributes ia = pSystem.imageAttributes;
                        ColorMatrix cm = pSystem.colorMatrix;
                        cm.Matrix33 = (float)opacity / 100;
                        ia.SetColorMatrix(cm);

                        x_left = (float)(wX - image.Width / 2);
                        y_top = (float)(wY - image.Height / 2);
                        g.DrawImage(image, new Rectangle((int)x_left, (int)y_top, image.Width, image.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, ia);
                    }
                }

                mx.Reset();
                g.Transform = mx;
            }
        }
    }
}
